// Copyright (C) 2010 David Sugar, Tycho Softworks.
//
// This file is part of GNU uCommon C++.
//
// GNU uCommon C++ is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU uCommon C++ is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with GNU uCommon C++.  If not, see <http://www.gnu.org/licenses/>.

#include <ucommon/secure.h>
#include <ctype.h>
#include <sys/stat.h>

using namespace UCOMMON_NAMESPACE;

class __LOCAL bookmark : public LinkedObject
{
private:
    bookmark();

public:
    static void create(void);

    fsys::offset_t offset;
};

static shell::flagopt helpflag('h',"--help",    _TEXT("display this list"));
static shell::flagopt althelp('?', NULL, NULL);
static shell::flagopt ignore('i', "--ignore", _TEXT("ignore errors"));
static shell::numericopt bufopt('b', "--buffer", _TEXT("size of buffer"), "xk");

static int exit_code = 0;
static const char *argv0 = "tail";
static char *buffer;
static size_t bufsize = 4096;
static memalloc mpager;
static LinkedObject *list;
static fsys::offset_t pos, prior;
static bool header = false;
static long lines = 10;
static char nl = '\n';

bookmark::bookmark() :
LinkedObject(&list)
{
    offset = prior;
    prior = pos + 1;
}

void bookmark::create(void)
{
    caddr_t addr = (caddr_t)mpager.zalloc(sizeof(bookmark));
    new(addr) bookmark;
}

static void report(const char *path, int code)
{
    const char *err = _TEXT("i/o error");

    switch(code) {
    case EACCES:
    case EPERM:
        err = _TEXT("permission denied");
        break;
    case EROFS:
        err = _TEXT("read-only file system");
        break;
    case ENODEV:
    case ENOENT:
        err = _TEXT("no such file or directory");
        break;
    case ENOTDIR:
        err = _TEXT("not a directory");
        break;
    case ENOTEMPTY:
        err = _TEXT("directory not empty");
        break;
    case ENOSPC:
        err = _TEXT("no space left on device");
        break;
    case EBADF:
    case ENAMETOOLONG:
        err = _TEXT("bad file path");
        break;
    case EBUSY:
    case EINPROGRESS:
        err = _TEXT("file or directory busy");
        break;
    case EINTR:
        err = _TEXT("operation interupted");
        break;
#ifdef  ELOOP
    case ELOOP:
        err = _TEXT("too many sym links");
        break;
#endif
    }

    if(is(ignore))
        fprintf(stderr, "%s: %s: %s\n", argv0, path, err);
    else
        shell::errexit(1, "*** %s: %s: %s\n", argv0, path, err);

    exit_code = 1;
}

static void output(fsys_t& fs, const char *path, bookmark *mark)
{
    fsys_t out = shell::output();
    ssize_t iolen = bufsize;
    int err;

    if(header)
        printf("%s:\n", path);

    if(!mark)
        err = fs.seek(0l);
    else
        err = fs.seek(mark->offset);

    if(err) {
        report(path, err);
        goto exit;
    }

    while(is(fs) && iolen > 0) {
        iolen = fs.read(buffer, bufsize);

        if(iolen < 0)
            report(path, fs.err());

        if(iolen < 1)
            break;

        out.write(buffer, iolen);
    }
exit:
    out.release();
}

static void tail(const char *path)
{
    fsys_t fs, tmp;
    ssize_t iopos, iolen = bufsize;
    long count = lines;

    mpager.purge();

    list = NULL;
    pos = prior = 0l;

    if(path && fsys::is_dir(path)) {
        fprintf(stderr, "%s: %s: %s\n", argv0, path, _TEXT("is a directory"));
        return;
    }

    if(!path) {
        const char *tmpname = shell::path(shell::PROGRAM_TEMP);
        path = "-";
        fs.set(shell::input());
        tmp.erase(tmpname);
        tmp.open(tmpname, 0600, fsys::RDWR);
        fsys::unlink(tmpname);
    }
    else
        fs.open(path, fsys::STREAM);

    if(!is(fs)) {
        report(path, fs.err());
        return;
    }

    iolen = bufsize;
    while(is(fs) && iolen > 0) {
        iolen = fs.read(buffer, bufsize);
        iopos = 0;
        while(iopos < iolen) {
            ++pos;
            if(buffer[iopos] == nl)
                bookmark::create();
            ++iopos;
        }
        if(iolen < 0)
            report(path, fs.err());

        if(iolen < 1)
            break;

        if(is(tmp)) {
            iolen = tmp.write(buffer, iolen);
            if(iolen < 0)
                report("-", fs.err());
        }

    }

    linked_pointer<bookmark> bp = list;
    if(lines < 0)
        count = -lines;
    else
        count = lines;

    if(count)
        --count;

    while(count-- && is(bp))
        bp.next();

    if(is(tmp)) {
        output(tmp, path, *bp);
        tmp.close();
        fs.release();
    }
    else {
        output(fs, path, *bp);
        fs.close();
    }
}

PROGRAM_MAIN(argc, argv)
{
    shell::bind("tail");
    shell::setNumeric(shell::NUMERIC_ALL);

    shell args(argc, argv);
    argv0 = args.argv0();
    unsigned count = 0;

    argv0 = args.argv0();

    if(is(helpflag) || is(althelp)) {
        printf("%s: %s %s\n", _TEXT("Usage"), argv0, _TEXT("[options] path..."));
        printf("%s\n\n", _TEXT("Output last -# lines of files"));
        printf("%s\n", _TEXT("Options:"));
        shell::help();
        printf("\n%s\n", _TEXT("Report bugs to dyfet@gnu.org"));
        PROGRAM_EXIT(0);
    }

    if(is(bufopt))
        bufsize = *bufopt * 1024l;

    if(bufsize < 1)
        shell::errexit(2, "*** %s: %ld: %s\n", argv0, (long)bufsize,
            _TEXT("invalid buffer size"));

    buffer = new char[bufsize];
    if(!buffer)
        shell::errexit(2, "*** %s: %ld: %s\n", argv0, (long)bufsize,
            _TEXT("cannot allocate buffer"));

    if(args() > 1)
        header = true;

    lines = shell::getNumeric();
    if(!lines)
        lines = 10;

    if(!args())
        tail(NULL);
    else while(count < args())
        tail(args[count++]);

    PROGRAM_EXIT(exit_code);
}

